(** Bombs-Must-Detonate: Final Compiler
    @author: Brian Go *)

open Bmdirc

type instruction = Bmdirc.instruction

(** Compilation environment location information for variables *)
type location = 
    StackLoc of int
  | GlobalLoc of int
  | FunctionLoc of string (** Function types are stored internally as strings.  The VM uses a reflection-esque mechanism to call a string.  This is safe b/c the typechecker is awesome. *)

(** Loop information for the environment to compile break & continue statements *)
type loop_info = 
    (** Break label, continue label *)
    LoopBreakContinue of string * string
  | LoopNoInfo

(** Environment keeps track of variables, stack restoration, and loop break/continue labels within different frames *)
type environment = 
    EnvTopLevel of int * (string * location) list
  | EnvFrame of int * loop_info * (string * location) list * environment


exception Compile_error of string

(** Raises a compiler error from the concatenation of the given string list *)
let raise_compile_error slist = raise (Compile_error (String.concat "" slist))

(** Makes a new compilation environment *)
let make_env () = (EnvTopLevel (0,[]))

(** Gets all variable definitions of the current stack frame and its parents*)
let rec get_cur_defs env = match env with
    EnvTopLevel (_,dlist) -> dlist
  | EnvFrame (_,_,dlist,parent) -> dlist @ (get_cur_defs parent)

(** Gets the location of the given variable *)
let get_loc v env = List.assoc v (get_cur_defs env)

(** Adds the location information of the given variable to the current stack frame *)
let add_loc (v,loc) env = match env with
    EnvTopLevel (n,dlist) -> EnvTopLevel (n,((v,loc)::dlist))
  | EnvFrame (n,info,dlist,parent) -> EnvFrame (n,info,((v,loc)::dlist),parent)

(** Checks if the given variable is defined in the current scope or one of its parent scopes *)
let isdef v env = List.mem_assoc v (get_cur_defs env)

(** Offsets every stack location in the environment by i (positive = push, negative = pop *)
let rec offset_locs i env = 
  let helper (x,y) = match y with
      StackLoc loc -> (x,StackLoc (loc+i))
    | z -> x,z
  in
    match env with 
	EnvTopLevel (n,vlist) -> 
	  EnvTopLevel (n+i, List.map helper vlist)
      | EnvFrame (n,info, vlist,parent) -> 
	  EnvFrame (n+i,info,List.map helper vlist, offset_locs i parent) 

(** Adds a frame to the environment *)
let add_frame env = EnvFrame (0,LoopNoInfo,[],env)

(** Removes a frame from the envrionment.  Offsets the parent assuming all remaining elements 
    in the frame were popped (see get_restore_code *)
let remove_frame env = match env with
    EnvTopLevel _ -> raise_compile_error ["Unexpected compilation error: attempted to pop toplevel compilation scope."]
  | EnvFrame (n,_,_,parent) -> offset_locs (-n) parent

(** Repeats the given list n times and concatenates the repetitions *)
let rec repeat_list lst n = 
  if n = 0 then []
  else if n = 1 then lst
  else if n > 1 then lst @ (repeat_list lst (n-1))
  else raise (Invalid_argument "repeat_list must have a nonnegative argument")

(** Gets the number of pops needed to restore the current stack frame to its parent *)
let get_restore_code env = match env with
    EnvTopLevel _ -> []
  | EnvFrame (n,_,_,_) -> repeat_list [Pop] n

(** Sets the break/continue information of the current environment frame *)
let set_loop_info info env = match env with
    EnvTopLevel _ -> raise_compile_error ["Unexpected compilation error: attempted to set loop information in top level."]
  | EnvFrame (n,_,defs,parent) -> EnvFrame (n,info,defs,parent)

(** Gets the relevant break label *)
let rec get_break_label env = match env with
    EnvTopLevel _ -> raise_compile_error ["Unexpected compilation error: attempted to break from a loop outside a loop."]
  | EnvFrame (_,info,_,parent) ->
      match info with
	  LoopNoInfo -> get_break_label parent
	| LoopBreakContinue (b,_) -> b

(** Gets the restore code needed to exit all frames that have been entered since the loop frame
    before breaking/continuing *)
let rec get_breakcontinue_restore env = match env with
    EnvTopLevel _ -> raise_compile_error ["Unexpected compilation error: attempted to break from a loop outside a loop."]
  | EnvFrame (n,info,_,parent) ->
      match info with
	  LoopNoInfo -> (repeat_list [Pop] n) @ get_breakcontinue_restore parent
	| LoopBreakContinue (_,_) -> repeat_list [Pop] n

(** Gets the relevant continue label *)
let rec get_continue_label env = match env with
    EnvTopLevel _ -> raise_compile_error ["Unexpected compilation error: attempted to continue a loop outside a loop."]
  | EnvFrame (_,info,_,parent) ->
      match info with
	  LoopNoInfo -> get_continue_label parent
	| LoopBreakContinue (_,c) -> c
 

    
(** See Bmdirc.make_label *)
let make_label str = Bmdirc.make_label str

(** Gets the change in stack pointer resulting from the given instruction.
    Note that this from a instruction-by-instruction point of view, not a code flow point of view. *)
let get_stack_offset inst = match inst with 
    Push -> 1
  | Pop -> -1
  | Read -> 0
  | Print -> 0
  | PrintLn -> 0
  | Swap -> 0
  | Stop -> 0
  | Return -> 0
  | Cons -> -1
  | Car -> 0
  | Cdr -> 0
  | IsNull -> 0
  | Nil -> 0
  | GetElem -> -1
  | SetElem -> -2
  | And -> -1
  | Or -> -1
  | Add -> -1
  | Sub -> -1
  | Mul -> -1
  | Div -> -1
  | DivI -> -1
  | RemI -> -1
  | Concat -> -1
  | Neg -> 0
  | Frac -> 0
  | Int -> 0
  | Lt -> -1
  | Gt -> -1
  | Lte -> -1
  | Gte -> -1
  | Eq -> -1
  | Neq -> -1
  | ConstUninit -> 0
  | ConstInt _ -> 0
  | ConstFloat _ -> 0
  | ConstString _ -> 0
  | PushSf _ -> 2
  | Assign _ -> 0
  | Acc _ -> 0
  | Rev _ -> 0
  | MakeBlock _ -> 0
  | AllocFields _ -> 0
  | GetField _ -> 0
  | SetField _ -> 0
  | Call _ -> 0
  | Apply -> 0
  | Jmp _ -> 0
  | Jz _ -> 0
  | Jnz _ -> 0
  | RPC _ -> 0
  | Label _ -> 0
  | Comment _ -> 0
  | Annotation _ -> 0
  | GetElemStatic _ -> 0
  | SetElemStatic _ -> -1
  | MakeBlockStatic (_,n) -> -n
  | MakeBlockFilled _ -> -1

(** Gets the number of global fields that need to be allocated *)
let rec n_globals env = 
  let rec helper lst = match lst with
      (_,GlobalLoc _)::rest -> 1 + (helper rest)
    | _::rest -> helper rest
    | [] -> 0
  in
    match env with 
	EnvTopLevel (_,vlist) -> helper vlist
      | EnvFrame (_,_,vlist,parent) -> (helper vlist) + (n_globals parent)

(** Compiles the given IR instruction *)
let rec compile_ir_instruction inst env = match inst with
    Instruction i -> [i], offset_locs (get_stack_offset i) env
  | GetVar v -> 
      if isdef v env then
	(match get_loc v env with
	     StackLoc loc ->
	       [Acc loc], env
	   | GlobalLoc loc ->
	       [GetField loc], env
	   | FunctionLoc funname ->
	       [ConstString funname], env)
      else
	raise_compile_error ["Error in instruction GetVar, ";v;" was not defined."]
  | SetVar v ->
      if isdef v env then
	(match get_loc v env with
	     StackLoc loc ->
	       [Assign loc], env
	   | GlobalLoc loc ->
	       [SetField loc], env
	   | FunctionLoc name ->
	       raise_compile_error ["Unexpected compile error: attempted to re-define function ";name;"."])
      else 
	[Push], add_loc (v,StackLoc 0) (offset_locs 1 env)
  | NoteVar (v,i) -> [], add_loc (v,StackLoc i) env
  | NoteFunction name -> [], add_loc (name,FunctionLoc name) env
  | If (test,thenbody,elsebody) ->
      let else_label = make_label "else" in
      let endif_label = make_label "endif" in
      let test_result,_ = compile_ir_instruction_list test env in
      let then_result,then_env = compile_ir_instruction_list thenbody (add_frame env) in
      let else_result,else_env = compile_ir_instruction_list elsebody (add_frame env) in
      let restore_code_then = get_restore_code then_env in
      let restore_code_else = get_restore_code else_env in
	[Comment "if"]
	@ test_result
	@ [Jz else_label]
	@ then_result
	@ restore_code_then
	@ [Jmp endif_label;
	   Label else_label]
	@ else_result
	@ restore_code_else
	@ [Label endif_label]
	@ [Comment "endif"], env
  | For (init, test, step, body) ->
      let test_label = make_label "for_test" in
      let endfor_label = make_label "for_end" in
      let init_result,init_env = compile_ir_instruction_list init (add_frame env) in
      let restore_init = get_restore_code init_env in
      let test_result,_ = compile_ir_instruction_list test init_env in
      let env_for_body = set_loop_info (LoopBreakContinue (endfor_label,test_label)) (add_frame init_env) in
      let body_result,body_env = compile_ir_instruction_list body env_for_body in
      let restore_body = get_restore_code body_env in
      let step_result,step_env = compile_ir_instruction_list step (add_frame init_env) in
      let restore_step = get_restore_code step_env in
	[Comment "for"]
	@ init_result
	@ [Label test_label]
	@ test_result
	@ [Jz endfor_label]
	@ body_result
	@ restore_body
	@ step_result
	@ restore_step
	@ [Jmp test_label]
	@ [Label endfor_label]
	@ restore_init
	@ [Comment "end for"], env
  | While (test, body) ->
      let test_label = make_label "while" in
      let wend_label = make_label "wend" in
      let test_result,_ = compile_ir_instruction_list test env in 
      let env_for_body = set_loop_info (LoopBreakContinue (wend_label,test_label)) (add_frame env) in
      let body_result,body_env = compile_ir_instruction_list body env_for_body in
      let restore_body = get_restore_code body_env in
	[Comment "while"]
	@ [Label test_label]
	@ test_result
	@ [Jz wend_label]
	@ body_result
	@ restore_body
	@ [Jmp test_label]
	@ [Label wend_label]
	@ [Comment "wend"], env
  | DoWhile (body,test) ->
      let do_label = make_label "do" in
      let end_label = make_label "end_dowhile" in
      let test_result,_ = compile_ir_instruction_list test env in 
      let env_for_body = set_loop_info (LoopBreakContinue (end_label,do_label)) (add_frame env) in
      let body_result,body_env = compile_ir_instruction_list body env_for_body in
      let restore_body = get_restore_code body_env in
	[Comment "dowhile"]
	@ [Label do_label]
	@ body_result
	@ restore_body
	@ [Comment "dowhile_test"]
	@ test_result
	@ [Jnz do_label]
	@ [Label end_label]
	@ [Comment "end dowhile"], env
  | Break -> 
      let restore_code = get_breakcontinue_restore env in
	restore_code @ [Jmp (get_break_label env)], env
  | Continue -> 
      let restore_code = get_breakcontinue_restore env in
	restore_code @ [Jmp (get_continue_label env)], env
  | BeginScope -> [], add_frame env
  | EndScope -> [], remove_frame env
  | EndScopeSf -> [], offset_locs (-2) (remove_frame env)
  | EndScopeSfRPC -> [], offset_locs (-1) (remove_frame env)
  | DeclareGlobal s ->
      let n = (n_globals env) in
	[], add_loc (s,GlobalLoc n) env

(** Compiles a list of IR instructions *)
and compile_ir_instruction_list ir_buf env = match ir_buf with
    cur::rest -> 
      let cur_result,new_env = compile_ir_instruction cur env in
      let rest_result,final_env = compile_ir_instruction_list rest new_env in
	cur_result @ rest_result, final_env
  | [] -> [], env

(** Compiles a complete buffer of IR instructions, including the compiler signal to allocate global fields *)
let compile_buffer ir_buf = 
  try
    let start_env = make_env () in
    let result,result_env = compile_ir_instruction_list ir_buf start_env in
      (AllocFields (n_globals result_env))::result
  with 
      Compile_error s ->
	let _ = output_string stderr s in
	let _ = output_string stderr "\n" in
	let _ = flush stderr in
	  raise (Compile_error s)

(** Produces a string representation of the given instruction buffer *)
let rec string_of_buffer buf = match buf with
    cur::rest -> String.concat "" [(Bmdirc.string_of_instruction cur);"\n";(string_of_buffer rest)]
  | [] -> ""

(** Prints the given instruction buffer to the given output channel *)
let print_buffer chan buf = 
  output_string chan (string_of_buffer buf)
